package com.mainbo.core.filter;

import com.mainbo.core.vo.Result;
import com.mainbo.platform.uc.client.LoginSuccessCallback;
import com.mainbo.platform.uc.client.RemoteJwtValidator;
import com.mainbo.platform.uc.client.SimpleRemoteJwtValidator;
import com.mainbo.platform.uc.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author xww
 * @Description //TODO
 * @Date 2020/3/16   9:23
 **/
public class ShiroCasUserFilter extends UserFilter {

    private static final Logger logger = LoggerFactory.getLogger(com.mainbo.platform.uc.client.ShiroCasUserFilter.class);

    /**
     * 是否本地验证jwt 的有效性。 如果本地验证，则平台超时与本地超时无关，及平台退出后，不会导致应用退出.
     * 并且本应用跳转其他应用时，如果平台中未登录，则会要求重新登录。
     */
    private boolean isLocalValid = false;

    /**
     * 当前应用的appKey
     */
    private String appKey;

    /**
     * 远程验证
     */
    private String validateUrl;

    /**
     * 远程验证器
     */
    private RemoteJwtValidator validator = new SimpleRemoteJwtValidator();

    /**
     * 认证成功后回调
     */
    private LoginSuccessCallback successCallback;

    /**
     * 是否使用本地登录
     */
    private Boolean useLocalLogin = false;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        boolean islogin = super.isAccessAllowed(request, response, mappedValue);

        if (useLocalLogin) {
            if (islogin) {
                localLogin(request, response);
            }
            return islogin;
        }
        // 获取cookie 中jwt,并验证jwt 参数
        String jwt = WebUtils.getCleanParam(request, JwtUtils.JWT_PARA_NAME);

        if (jwt == null) {
            jwt = (String) ((HttpServletRequest) request).getSession().getAttribute(JwtUtils.JWT_PARA_NAME);
        } else {
            islogin = false;
        }

        boolean valid = validateJwt(jwt, islogin, request, response);

        if (valid) {
            areadyLoin();
            if (!islogin) {
                ((HttpServletRequest) request).getSession().setAttribute(JwtUtils.JWT_PARA_NAME, jwt);
            }
        }

        return valid;

    }

    protected void localLogin(ServletRequest request, ServletResponse response) {

    }

    private boolean validateJwt(String jwt, boolean isLogin, ServletRequest request, ServletResponse response) {
        if (StringUtils.isEmpty(jwt)) {
            return false;
        }
        if (isLocalValid) {
            logger.debug("login with jwt : {}", jwt);
            try {
                if (!isLogin) { // 本地验证，只进行登录后 验证
                    Claims clm = JwtUtils.verifyTokenWithBody(jwt, appKey);
                    validateSuccess(clm.getSubject(), clm, request, response);
                }
                return true;
            } catch (JwtException e) {
                logger.debug("login valid failed, jwt:{},appkey:{} ,\n msg:{} ", jwt, appKey, e.getMessage());
            } catch (Exception e) {
                logger.warn("validate failed!", e);
            }
        } else {
            Result rs = validator.validate(validateUrl, jwt, appKey);
            if (Result.SUCCESS.equals(rs.getCode())) { // 成功
                if (!isLogin) { // 本地未登录处理
                    Claims clm = JwtUtils.decodeToken(jwt);
                    validateSuccess(clm.getSubject(), clm, request, response);
                }
                return true;
            } else if (isLogin) {
                HttpSession session = ((HttpServletRequest) request).getSession();
                if (session != null) {
                    Enumeration<String> names = session.getAttributeNames();
                    while (names.hasMoreElements()) {
                        String name = names.nextElement();
                        session.removeAttribute(name);
                    }
                }
            }
        }

        return false;
    }

    protected void areadyLoin() {
    }

    /**
     * 本地验证成功后调用
     */
    private void validateSuccess(String sub, Map<String, Object> params, ServletRequest request,
                                 ServletResponse response) {
        if (successCallback != null) {
            successCallback.onSuccess(sub, params, request, response);
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        logger.debug("denied url: {}", ((HttpServletRequest) request).getRequestURI());
        saveRequestAndRedirectToLogin(request, response);
        return false;
    }

    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        Map<String, String> appKeyMap = new HashMap<String, String>();
        if (!useLocalLogin) {
            appKeyMap.put(JwtUtils.APP_KEY, appKey);
            String queryString = ((HttpServletRequest) request).getQueryString();
            String oldjwt = request.getParameter(JwtUtils.APP_KEY);
            if (StringUtils.isEmpty(queryString) || oldjwt != null) {
                appKeyMap.put(JwtUtils.REDIRECT_URL, ((HttpServletRequest) request).getRequestURL().toString());
            } else {
                appKeyMap.put(JwtUtils.REDIRECT_URL,
                        ((HttpServletRequest) request).getRequestURL().append("?").append(queryString).toString());
            }
        }

        String loginUrl = getLoginUrl();
        WebUtils.issueRedirect(request, response, loginUrl, appKeyMap, true, true);
    }

    /**
     * Getter method for property <tt>appKey</tt>.
     *
     * @return property value of appKey
     */
    public String getAppKey() {
        return appKey;
    }

    /**
     * Setter method for property <tt>appKey</tt>.
     *
     * @param appKey
     *          value to be assigned to property appKey
     */
    public void setAppKey(String appKey) {
        this.appKey = appKey;
    }

    /**
     * Getter method for property <tt>isLocalValid</tt>.
     *
     * @return property value of isLocalValid
     */
    public boolean isLocalValid() {
        return isLocalValid;
    }

    /**
     * Setter method for property <tt>isLocalValid</tt>.
     */
    public void setLocalValid(boolean isLocalValid) {
        this.isLocalValid = isLocalValid;
    }

    /**
     * Getter method for property <tt>validateUrl</tt>.
     *
     * @return property value of validateUrl
     */
    public String getValidateUrl() {
        return validateUrl;
    }

    /**
     * Setter method for property <tt>validateUrl</tt>.
     *
     * @param validateUrl
     *          value to be assigned to property validateUrl
     */
    public void setValidateUrl(String validateUrl) {
        this.validateUrl = validateUrl;
    }

    /**
     * Getter method for property <tt>validator</tt>.
     *
     * @return property value of validator
     */
    public RemoteJwtValidator getValidator() {
        return validator;
    }

    /**
     * Setter method for property <tt>validator</tt>.
     *
     * @param validator
     *          value to be assigned to property validator
     */
    public void setValidator(RemoteJwtValidator validator) {
        this.validator = validator;
    }

    /**
     * Getter method for property <tt>successCallback</tt>.
     *
     * @return property value of successCallback
     */
    public LoginSuccessCallback getSuccessCallback() {
        return successCallback;
    }

    /**
     * Setter method for property <tt>successCallback</tt>.
     *
     * @param successCallback
     *          value to be assigned to property successCallback
     */
    public void setSuccessCallback(LoginSuccessCallback successCallback) {
        this.successCallback = successCallback;
    }

    /**
     * Setter method for property <tt>useLocalLogin</tt>.
     *
     * @param useLocalLogin
     *          Boolean value to be assigned to property useLocalLogin
     */
    public void setUseLocalLogin(Boolean useLocalLogin) {
        this.useLocalLogin = useLocalLogin == null ? Boolean.FALSE : useLocalLogin;
    }

}
